package cn.shop.merchant.service;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.shop.merchant.config.HxPayConfig;
import cn.shop.merchant.config.FkPayConfig;
import cn.shop.merchant.config.PayConfig;
import cn.shop.merchant.req.FkNotifyReq;
import cn.shop.merchant.req.NotifyReq;
import cn.shop.merchant.req.RechargeRecordQueryReq;
import cn.shop.merchant.util.IpUtil;
import cn.shop.transcation.TransactionRecord.entity.RechargeInfoReq;
import cn.shop.transcation.backPaymentChannelModule.entity.BackPaymentChannel;
import cn.shop.transcation.backPaymentChannelModule.mapper.BackPaymentChannelMapper;
import cn.shop.transcation.detail.entity.FundDetail;
import cn.shop.transcation.detail.mapper.FundDetailMapper;
import cn.shop.transcation.merchantUser.mapper.MerchantUserMapper;
import cn.shop.transcation.merchantUser.mapper.entity.MerchantUser;
import cn.shop.transcation.promotionsModule.Promotions;
import cn.shop.transcation.rechargerecord.entity.Record;
import cn.shop.transcation.rechargerecord.mapper.RecordMapper;
import cn.shop.utils.PageUtil;
import cn.shop.utils.PayUtil;
import cn.shop.utils.Putif;
import cn.shop.utils.constant.PromotionsConstant;
import cn.shop.utils.enums.RechargeSolution;
import cn.shop.utils.enums.TransactionState;
import cn.shop.utils.exception.ServiceException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author : 李一涵
 * @date : Created in 2021-11-10 9:12
 * @description : 充值记录service
 */
@Slf4j
@Service
public class RechargeRecordService extends BaseServiceImpl<RecordMapper, Record>{

    @Resource
    private MerchantUserMapper userMapper;
    @Resource
    private BackPaymentChannelMapper channelMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private PayConfig payConfig;
    @Resource
    private HxPayConfig hxPayConfig;
    @Resource
    private FkPayConfig fkPayConfig;
    @Resource
    private FundDetailMapper fundDetailMapper;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private PromotionsService promotionsService;

    /**
     * 获取充值记录列表
     *
     * @param uid 商户编号
     * @param req 查询条件
     */
    public Map<String, Object> records(long uid, RechargeRecordQueryReq req) {
        Page<Record> page = Page.of(req.getCurrent(), req.getSize());
        LambdaQueryWrapper<Record> query = Wrappers.<Record>lambdaQuery()
                .eq(Record::getPlayerId, uid)
                .eq(Record::getState, TransactionState.COMPLETED)
                .apply(notNull.test(req.getStartTime()), req.gtTime("create_time"), req.getStartTime())
                .apply(notNull.test(req.getEndTime()), req.ltTime("create_time"), req.getEndTime())
                .orderByDesc(Record::getId);
        List<BackPaymentChannel> channels = channelMapper.selectList(Wrappers.emptyWrapper());
        Map<Long, RechargeSolution> map = channels.stream().collect(Collectors.toMap(BackPaymentChannel::getId, BackPaymentChannel::getType));
        page = page(page, query);
        return PageUtil.formatPage(page, p -> format(p, map));
    }

    public List<BackPaymentChannel> channelList() {
        return channelMapper.selectList(Wrappers.<BackPaymentChannel>lambdaQuery()
                .eq(BackPaymentChannel::getDefaultChannel, true));
    }

    @Transactional(rollbackFor = Exception.class)
    public Object recharge(long uid, RechargeInfoReq rechargeInfoReq, HttpServletRequest request) {
        String scheme = request.getHeader("origin");
        String ipStr = IpUtil.getClientIp();
        long ip = NetUtil.ipv4ToLong(ipStr);
        MerchantUser user = userMapper.selectById(uid);
        BackPaymentChannel channel = channelMapper.selectById(rechargeInfoReq.getRechargeSolution());
        if (channel == null || channel.getType() == null
                || StrUtil.hasBlank(channel.getSecret())
                || channel.getMerchantNumber() == null) {
            throw new ServiceException("支付方式错误");
        }
        BigDecimal rate = BigDecimal.ZERO;
        BigDecimal amount = Convert.toBigDecimal(rechargeInfoReq.getAmount());
        if (channel.getRate() != null) {
            BigDecimal charges = channel.getRate().divide(new BigDecimal(100), 4, RoundingMode.CEILING);
            if (charges.compareTo(BigDecimal.ZERO) > 0) {
                rate = amount.multiply(charges);
            }
        }

        Record record = new Record();
        record.setState(TransactionState.PENDING_PAYMENT);
        record.setPlayerName(user.getAccount());
        record.setPlayerId(user.getId());
        record.setAmount(amount);
        record.setActualAmount(amount.subtract(rate));
        record.setBalance(user.getBalance().add(record.getActualAmount()));
        record.setChannel(channel.getType().getDescription() + "#" + channel.getName());
        record.setIp(ipStr);
        record.setChannelId(channel.getId());
        record.setType(1);

        boolean insert = save(record);
        if (insert) {
            //通过redis 的过期监听机制 过期后触发 自动取消订单
            stringRedisTemplate.opsForValue().set("orderTR-" + record.getId(), "0", 5, TimeUnit.MINUTES);
            log.info("开始设置充值订单 {} 的redis超时key：{}", record.getId(), "orderTR-" + record.getId());
            String notifyUrl;
            switch (channel.getApiType()) {
                case DY_PAYMENT:
                    notifyUrl = payConfig.getNotifyUrl()
                            .replace("{origin}", scheme);
                    return sendPay(amount, record.getId(), ip, channel,
                            rechargeInfoReq.getReturnUrl(), notifyUrl, rechargeInfoReq.getBankCode());
                case HX_PAYMENT:
                    notifyUrl = hxPayConfig.getNotifyUrl()
                            .replace("{origin}", scheme);
                    return sendPay(amount, record.getId(), ip, channel,
                            rechargeInfoReq.getReturnUrl(), notifyUrl, rechargeInfoReq.getBankCode());
                case FK_PAYMENT:
                    notifyUrl = fkPayConfig.getNotifyUrl().replace("{origin}", scheme);
                    return sendPay(amount, record.getId(), ipStr, channel,
                            rechargeInfoReq.getReturnUrl(), notifyUrl, rechargeInfoReq.getBankCode());
                default:
                    throw new ServiceException("支付渠道错误");
            }

        }
        else {
            throw new ServiceException("服务器异常,暂时无法交易");
        }
    }

    public String notify(NotifyReq payCallback) {
        RLock lock = redissonClient.getLock("notify:" + payCallback.getOrder_id());
        try {
            if (lock.tryLock(0, 30, TimeUnit.SECONDS)) {
                LambdaQueryWrapper<Record> query = Wrappers.lambdaQuery();
                query.eq(Record::getId, payCallback.getOut_trade_no()).ne(Record::getState, TransactionState.CANCELLED);
                Record record = getOne(query);
                //校验订单
                if (record == null) {
                    log.error("充值订单:{} , 支付订单:{}, 订单无效", payCallback.getOut_trade_no(), payCallback.getOrder_id());
                    return "fail";
                }
                BackPaymentChannel channel = channelMapper.selectById(record.getChannelId());
                if (channel == null || channel.getType() == null
                        || StrUtil.hasBlank(channel.getSecret())
                        || channel.getMerchantNumber() == null) {
                    throw new ServiceException("支付方式错误");
                }
                Dict dict = Dict.create();
                dict.set("amount", payCallback.getAmount()).
                        set("out_trade_no", payCallback.getOut_trade_no()).
                        set("order_pay", payCallback.getOrder_pay()).
                        set("merchant_id", payCallback.getMerchant_id()).
                        set("order_id", payCallback.getOrder_id());
                //校验签名
                if (ObjectUtil.notEqual(PayUtil.getSignToken(dict, channel.getSecret()), payCallback.getCode())) {
                    log.error("充值订单:{} , 支付订单:{}, 回调签名认证失败", payCallback.getOut_trade_no(), payCallback.getOrder_id());
                    return "fail";
                }

                // 订单已经处理过 , 直接返回success
                if (record.getState().getCode() >= TransactionState.COMPLETED.getCode()) {
                    log.info("充值订单:{} , 支付订单:{}, 充值成功", payCallback.getOut_trade_no(), payCallback.getOrder_id());
                    return "SUCCESS";
                }
                //校验金额
                if (!NumberUtil.equals(payCallback.getAmount(), NumberUtil.mul(record.getAmount(), 100))) {
                    // throw new ServiceException("付款金额与订单金额不符");
                    log.error("充值订单:{} , 支付订单:{}, 付款金额与订单金额不符", payCallback.getOut_trade_no(), payCallback.getOrder_id());
                    return "FAIL";
                }
                // 修改充值状态
                record.setState(TransactionState.COMPLETED);
                updateById(record);
                this.processOrder(record, channel);
            } else {
                log.error("充值订单:{} , 支付订单:{}, 正在处理中", payCallback.getOut_trade_no(), payCallback.getOrder_id());
                return "fail";
            }
        } catch (Exception e) {
            log.error("充值订单:{} , 支付订单:{}, 支付回调失败,错误信息 :{} ", payCallback.getOut_trade_no(), payCallback.getOrder_id(), e.getMessage());
            return "FAIL";
        }
        log.info("充值订单:{} , 支付订单:{}, 支付回调处理完成", payCallback.getOut_trade_no(), payCallback.getOrder_id());
        return "SUCCESS";
    }

    public String fkNotify(FkNotifyReq payCallback) {
        RLock lock = redissonClient.getLock("notify:" + payCallback.getRequestId());
        try {
            if (lock.tryLock(0, 30, TimeUnit.SECONDS)) {
                LambdaQueryWrapper<Record> query = Wrappers.lambdaQuery();
                query.eq(Record::getId, payCallback.getRequestId()).ne(Record::getState, TransactionState.CANCELLED);
                Record record = getOne(query);
                //校验订单
                if (record == null) {
                    log.error("充值订单:{} , 支付订单:{}, 订单无效", payCallback.getRequestId(), payCallback.getOrderId());
                    return "fail";
                }
                BackPaymentChannel channel = channelMapper.selectById(record.getChannelId());
                if (channel == null || channel.getType() == null
                        || StrUtil.hasBlank(channel.getSecret())
                        || channel.getMerchantNumber() == null) {
                    throw new ServiceException("支付方式错误");
                }
                Dict dict = Dict.create();
                dict.set("amount", payCallback.getAmount()).
                        set("requestId", payCallback.getRequestId()).
                        set("orderId", payCallback.getOrderId()).
                        set("status", payCallback.getStatus()).
                        set("merchantId", payCallback.getMerchantId());
                //校验签名
                if (ObjectUtil.notEqual(PayUtil.getSignToken(dict, channel.getSecret()), payCallback.getCode())) {
                    log.error("充值订单:{} , 支付订单:{}, 回调签名认证失败", payCallback.getRequestId(), payCallback.getOrderId());
                    return "fail";
                }

                // 订单已经处理过 , 直接返回success
                if (record.getState().getCode() >= TransactionState.COMPLETED.getCode()) {
                    log.info("充值订单:{} , 支付订单:{}, 充值成功", payCallback.getRequestId(), payCallback.getOrderId());
                    return "SUCCESS";
                }
                //校验金额
                if (!NumberUtil.equals(payCallback.getAmount(), NumberUtil.mul(record.getAmount(), 100))) {
                    // throw new ServiceException("付款金额与订单金额不符");
                    log.error("充值订单:{} , 支付订单:{}, 付款金额与订单金额不符", payCallback.getRequestId(), payCallback.getOrderId());
                    return "FAIL";
                }
                if (!"SUCCESS".equals(payCallback.getStatus())) {
                    log.error("充值订单:{} , 支付订单:{}, 回调返回状态失败", payCallback.getRequestId(), payCallback.getOrderId());
                    return "FAIL";
                }
                // 修改充值状态
                record.setState(TransactionState.COMPLETED);
                updateById(record);
                this.processOrder(record, channel);
            } else {
                log.error("充值订单:{} , 支付订单:{}, 正在处理中", payCallback.getRequestId(), payCallback.getOrderId());
                return "fail";
            }
        } catch (Exception e) {
            log.error("充值订单:{} , 支付订单:{}, 支付回调失败,错误信息 :{} ", payCallback.getRequestId(), payCallback.getOrderId(), e.getMessage());
            return "FAIL";
        }
        log.info("充值订单:{} , 支付订单:{}, 支付回调处理完成", payCallback.getRequestId(), payCallback.getOrderId());
        return "SUCCESS";
    }

    private Object sendPay(BigDecimal amount, Long outTradeNo, long ip, BackPaymentChannel channel, String returnUrl, String notifyUrl, String bankCode) {
        Dict dict = Dict.create();
        //价格*100 转换成分
        dict.set("amount", NumberUtil.mul(amount, 100)).
                set("out_trade_no", outTradeNo).
                set("client_ip", ip).
                set("merchant_id", channel.getMerchantNumber()).
                set("notify_url", notifyUrl).
                set("channel_type", channel.getType().getKey()).
                set("return_url", returnUrl);
        if (channel.getType() == RechargeSolution.UPAY_PAYMENT) {
            dict.set("bank_code", bankCode);
        }
        String code = PayUtil.getSignToken(dict, channel.getSecret());
        log.info("支付渠道：{}, 支付接口返回code：{}", channel, code);
        // 创建支付 订单
        String url;
        String post;
        switch (channel.getApiType()) {
            case DY_PAYMENT:
                dict.set("code", code);
                url = payConfig.getUrl();
                log.info("payConfig={}", payConfig);
                post = HttpUtil.post(url, dict);
                break;
            case HX_PAYMENT:
                dict.set("code", code);
                url = hxPayConfig.getUrl();
                log.info("hxpayConfig={}", hxPayConfig);
                post = HttpUtil.post(url, dict);
                break;
            default:
                throw new ServiceException("支付渠道错误");
        }

        log.info("支付渠道：{},\n支付地址：{}，\n回调地址：{}\n支付参数：{}，\n支付接口返回结果：{}", channel, url, notifyUrl, dict, post);
        JSONObject map = JSONUtil.parseObj(post);
        int apiCode = (int) map.get("api_code");
        if (apiCode != 200) {
            throw new ServiceException((String) map.get("api_msg"));
        }
        else {
            return map.get("api_msg");
        }
    }

    private Object sendPay(BigDecimal amount, Long outTradeNo, String ip, BackPaymentChannel channel, String returnUrl, String notifyUrl, String bankCode) {
        Dict dict = Dict.create();
        //价格*100 转换成分
        dict.set("amount", NumberUtil.mul(amount, 100).intValue())
                .set("requestId", outTradeNo)
                .set("clientIp", ip)
                .set("merchantId", channel.getMerchantNumber())
                .set("notifyUrl", notifyUrl)
                .set("channelType", channel.getType().getKey())
                .set("returnUrl", returnUrl)
                .set("gameName", "好交易商户平台");
        if (channel.getType() == RechargeSolution.UPAY_PAYMENT) {
            dict.set("bankCode", bankCode);
        }
        String code = PayUtil.getSignToken(dict, channel.getSecret());
        dict.set("code", code);
        log.info("支付渠道：{}, 支付接口返回code：{}", channel, code);
        // 创建支付 订单
        log.info("nPayConfig={}", fkPayConfig);
        String url = fkPayConfig.getUrl();
        String json = JSONUtil.toJsonStr(dict);
        String post = HttpUtil.post(url, json);
        log.info("支付渠道：{},\n支付地址：{}，\n回调地址：{}\n支付参数：{}，\n支付接口返回结果：{}", channel, url, notifyUrl, dict, post);
        JSONObject map = JSONUtil.parseObj(post);
        int apiCode = (int) map.get("code");
        if (apiCode != 10000) {
            throw new ServiceException((String) map.get("msg"));
        }
        else {
            return map.get("data");
        }
    }

    @Async("taskExecutor")
    @Transactional(rollbackFor = Exception.class)
    public void processOrder(Record record, BackPaymentChannel channel) {
        if (channel.getType() == RechargeSolution.UPAY_PAYMENT) {
            Promotions promotions = promotionsService.getOne(Wrappers.<Promotions>lambdaQuery()
                    .eq(Promotions::getKey, PromotionsConstant.PRIZE_POOL));
            if (promotions.getEnable() && NumberUtil.isGreater(promotions.getPrizePool(), BigDecimal.ZERO)) {
                BigDecimal rate = record.getAmount().subtract(record.getActualAmount());
                boolean isSuccess = promotionsService.update(Wrappers.<Promotions>lambdaUpdate()
                        .setSql("prize_pool = prize_pool - " + rate)
                        .eq(Promotions::getId, 1)
                        .gt(Promotions::getPrizePool, BigDecimal.ZERO));
                if (isSuccess) {
                    promotions = promotionsService.getOne(Wrappers.<Promotions>lambdaQuery()
                            .eq(Promotions::getKey, PromotionsConstant.PRIZE_POOL));
                    BigDecimal actualAmount = NumberUtil.add(record.getActualAmount(), rate);
                    if (NumberUtil.isLess(promotions.getPrizePool(), BigDecimal.ZERO)) { //奖池金额小于0
                        if (NumberUtil.isLess(promotions.getPrizePool().abs(), rate)) { //奖池剩余金额的绝对值小于手续费，需要补回奖池里的金额
                            log.info("用户{} 充值{} 实际到账金额{}，从活动奖池中补回手续费{}，奖池剩余不足{}，需从中扣除{}",
                                    record.getPlayerId(), record.getAmount(), actualAmount, rate, promotions.getPrizePool(), promotions.getPrizePool());
                            record.setActualAmount(NumberUtil.sub(actualAmount, promotions.getPrizePool()));
                        } else {
                            log.info("用户{} 充值{} 实际到账金额{}，从活动奖池中补回手续费{}，奖池剩余不足{}，需从中扣除{}",
                                    record.getPlayerId(), record.getAmount(), actualAmount, rate, promotions.getPrizePool(), rate);
                            record.setActualAmount(NumberUtil.sub(actualAmount, rate));
                        }
                    } else {
                        log.info("用户{} 充值{} 实际到账金额{}，从活动奖池中补回手续费{}，奖池剩余{}",
                                record.getPlayerId(), record.getAmount(), actualAmount, rate, promotions.getPrizePool());
                        record.setActualAmount(actualAmount);
                    }
                    updateById(record);
                }
            }
        }
        MerchantUser user = new MerchantUser();
        LambdaUpdateWrapper<MerchantUser> updateWrapper = Wrappers.lambdaUpdate();
        // 给玩家添加余额
        updateWrapper.setSql("balance = balance + " + record.getActualAmount())
                .eq(MerchantUser::getId, record.getPlayerId());
        userMapper.update(user, updateWrapper);
        //添加交易记录
        FundDetail fundDetail = new FundDetail();
        fundDetail.setPlayerAccount(record.getPlayerName());
        fundDetail.setGoods("商户充值");
        fundDetail.setAmount(record.getActualAmount());
        fundDetail.setOrderId(record.getId());
        fundDetail.setIp(record.getIp());
        fundDetail.setPlayerId(record.getPlayerId());
        fundDetail.setBalance(record.getBalance());
        fundDetail.setDealTime(LocalDateTime.now());
        fundDetail.setPaySolution(channel.getType().getDescription() + "#" + channel.getName());
        fundDetail.setChannelShare(record.getAmount().subtract(record.getActualAmount()));
        fundDetail.setAlias("www");
        fundDetail.setType(2);
        fundDetailMapper.insert(fundDetail);
    }

    /**
     * 格式化充值记录
     *
     * @param record 充值记录
     * @return 格式化以后的结果
     */
    private Map<String, Object> format(Record record, Map<Long, RechargeSolution> map) {
        RechargeSolution solution = map.get(record.getChannelId());
        return Putif.start()
                .anyway("id", record.getId())
                .anyway("uid", record.getUid())
                .anyway("account", record.getPlayerName())
                .anyway("amount", record.getAmount())
                .anyway("balance", record.getBalance())
                .anyway("ip", record.getIp())
                .anyway("paySolution", solution != null ? solution.getDescription() : "")
                .anyway("createTime", record.getCreateTime())
                .get();
    }

}
